Un guide complet pour déboguer les coroutines Python asyncio à l’aide du mode de débogage intégré. Apprenez à identifier et à résoudre les problèmes courants de programmation asynchrone pour des applications robustes.
Débogage des coroutines Python : Maîtriser le mode de débogage Asyncio
La programmation asynchrone avec asyncio
en Python offre des avantages de performance considérables, en particulier pour les opérations liées aux E/S. Cependant, le débogage du code asynchrone peut être difficile en raison de son flux d’exécution non linéaire. Python fournit un mode de débogage intégré pour asyncio
qui peut grandement simplifier le processus de débogage. Ce guide explorera comment utiliser efficacement le mode de débogage asyncio
pour identifier et résoudre les problèmes courants dans vos applications asynchrones.
Comprendre les défis de la programmation asynchrone
Avant de plonger dans le mode de débogage, il est important de comprendre les défis courants du débogage du code asynchrone :
- Exécution non linéaire : Le code asynchrone ne s’exécute pas séquentiellement. Les coroutines rendent le contrôle à la boucle d’événements, ce qui rend difficile le traçage du chemin d’exécution.
- Changement de contexte : Les changements de contexte fréquents entre les tâches peuvent masquer la source des erreurs.
- Propagation des erreurs : Les erreurs dans une coroutine peuvent ne pas être immédiatement apparentes dans la coroutine appelante, ce qui rend difficile l’identification de la cause première.
- Conditions de concurrence : Les ressources partagées auxquelles plusieurs coroutines accèdent simultanément peuvent entraîner des conditions de concurrence, ce qui entraîne un comportement imprévisible.
- Interblocages : Les coroutines qui s’attendent indéfiniment les unes les autres peuvent provoquer des interblocages, ce qui arrête l’application.
Présentation du mode de débogage Asyncio
Le mode de débogage asyncio
fournit des informations précieuses sur l’exécution de votre code asynchrone. Il offre les fonctionnalités suivantes :
- Journalisation détaillée : Enregistre divers événements liés à la création, à l’exécution, à l’annulation et à la gestion des exceptions de la coroutine.
- Avertissements de ressources : Détecte les sockets non fermés, les fichiers non fermés et autres fuites de ressources.
- Détection des rappels lents : Identifie les rappels qui mettent plus de temps qu’un seuil spécifié à s’exécuter, ce qui indique des goulots d’étranglement potentiels des performances.
- Suivi de l’annulation des tâches : Fournit des informations sur l’annulation des tâches, vous aidant à comprendre pourquoi les tâches sont annulées et si elles sont gérées correctement.
- Contexte d’exception : Offre plus de contexte aux exceptions soulevées dans les coroutines, ce qui facilite le traçage de l’erreur jusqu’à sa source.
Activation du mode de débogage Asyncio
Vous pouvez activer le mode de débogage asyncio
de plusieurs manières :
1. Utilisation de la variable d’environnement PYTHONASYNCIODEBUG
La façon la plus simple d’activer le mode de débogage est de définir la variable d’environnement PYTHONASYNCIODEBUG
sur 1
avant d’exécuter votre script Python :
export PYTHONASYNCIODEBUG=1
python your_script.py
Cela activera le mode de débogage pour l’ensemble du script.
2. Définition de l’indicateur de débogage dans asyncio.run()
Si vous utilisez asyncio.run()
pour démarrer votre boucle d’événements, vous pouvez passer l’argument debug=True
 :
import asyncio
async def main():
print("Bonjour, asyncio !")
if __name__ == "__main__":
asyncio.run(main(), debug=True)
3. Utilisation de loop.set_debug()
Vous pouvez également activer le mode de débogage en obtenant l’instance de boucle d’événements et en appelant set_debug(True)
 :
import asyncio
async def main():
print("Bonjour, asyncio !")
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.set_debug(True)
loop.run_until_complete(main())
Interprétation de la sortie de débogage
Une fois le mode de débogage activé, asyncio
générera des messages de journal détaillés. Ces messages fournissent des informations précieuses sur l’exécution de vos coroutines. Voici quelques types courants de sortie de débogage et comment les interpréter :
1. Création et exécution de la coroutine
Le mode de débogage enregistre quand les coroutines sont créées et démarrées. Cela vous aide à suivre le cycle de vie de vos coroutines :
asyncio | execute <Task pending name='Task-1' coro=<a>() running at example.py:3>
asyncio | Task-1: created at example.py:7
Cette sortie montre qu’une tâche nommée Task-1
a été créée à la ligne 7 de example.py
et exécute actuellement la coroutine a()
définie à la ligne 3.
2. Annulation de tâche
Lorsqu’une tâche est annulée, le mode de débogage enregistre l’événement d’annulation et la raison de l’annulation :
asyncio | Task-1: cancelling
asyncio | Task-1: cancelled by <Task pending name='Task-2' coro=<b>() running at example.py:10>
Cela indique que Task-1
a été annulé par Task-2
. Comprendre l’annulation des tâches est essentiel pour prévenir un comportement inattendu.
3. Avertissements de ressources
Le mode de débogage met en garde contre les ressources non fermées, telles que les sockets et les fichiers :
ResourceWarning: unclosed <socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=6, laddr=('127.0.0.1', 5000), raddr=('127.0.0.1', 60000)
Ces avertissements vous aident à identifier et à corriger les fuites de ressources, ce qui peut entraîner une dégradation des performances et une instabilité du système.
4. Détection des rappels lents
Le mode de débogage peut détecter les rappels qui mettent plus de temps qu’un seuil spécifié à s’exécuter. Cela vous aide à identifier les goulots d’étranglement des performances :
asyncio | Task was destroyed but it is pending!
pending time: 12345.678 ms
5. Gestion des exceptions
Le mode de débogage fournit plus de contexte aux exceptions soulevées dans les coroutines, y compris la tâche et la coroutine où l’exception s’est produite :
asyncio | Task exception was never retrieved
future: <Task finished name='Task-1' coro=<a>() done, raised ValueError('Invalid value')>
Cette sortie indique qu’une ValueError
a été soulevée dans Task-1
et n’a pas été correctement gérée.
Exemples pratiques de débogage avec le mode de débogage Asyncio
Examinons quelques exemples pratiques de la façon d’utiliser le mode de débogage asyncio
pour diagnostiquer les problèmes courants :
1. Détection des sockets non fermés
Considérez le code suivant qui crée un socket mais ne le ferme pas correctement :
import asyncio
import socket
async def handle_client(reader, writer):
data = await reader.read(100)
message = data.decode()
addr = writer.get_extra_info('peername')
print(f"Received {message!r} from {addr!r}")
print(f"Send: {message!r}")
writer.write(data)
await writer.drain()
# Missing: writer.close()
async def main():
server = await asyncio.start_server(
handle_client,
'127.0.0.1',
8888
)
addr = server.sockets[0].getsockname()
print(f'Serving on {addr}')
async with server:
await server.serve_forever()
if __name__ == "__main__":
asyncio.run(main(), debug=True)
Lorsque vous exécutez ce code avec le mode de débogage activé, vous verrez un ResourceWarning
indiquant un socket non fermé :
ResourceWarning: unclosed <socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=6, laddr=('127.0.0.1', 8888), raddr=('127.0.0.1', 54321)>
Pour résoudre ce problème, vous devez vous assurer que le socket est fermé correctement, par exemple, en ajoutant writer.close()
dans la coroutine handle_client
et en l’attendant :
writer.close()
await writer.wait_closed()
2. Identification des rappels lents
Supposons que vous ayez une coroutine qui effectue une opération lente :
import asyncio
import time
async def slow_function():
print("Starting slow function")
time.sleep(2)
print("Slow function finished")
return "Result"
async def main():
task = asyncio.create_task(slow_function())
result = await task
print(f"Result: {result}")
if __name__ == "__main__":
asyncio.run(main(), debug=True)
Bien que la sortie de débogage par défaut n’indique pas directement les rappels lents, la combinaison avec une journalisation et des outils de profilage minutieux (comme cProfile ou py-spy) vous permet de réduire les parties lentes de votre code. Envisagez d’enregistrer les horodatages avant et après les opérations potentiellement lentes. Des outils comme cProfile peuvent ensuite être utilisés sur les appels de fonction enregistrés pour isoler les goulots d’étranglement.
3. Débogage de l’annulation de tâche
Considérez un scénario où une tâche est annulée de façon inattendue :
import asyncio
async def worker():
try:
while True:
print("Working...")
await asyncio.sleep(0.5)
except asyncio.CancelledError:
print("Worker cancelled")
async def main():
task = asyncio.create_task(worker())
await asyncio.sleep(2)
task.cancel()
try:
await task
except asyncio.CancelledError:
print("Task cancelled in main")
if __name__ == "__main__":
asyncio.run(main(), debug=True)
La sortie de débogage affichera la tâche en cours d’annulation :
asyncio | execute <Task pending name='Task-1' coro=<worker() running at example.py:3> started at example.py:16>
Working...
Working...
Working...
Working...
asyncio | Task-1: cancelling
Worker cancelled
asyncio | Task-1: cancelled by <Task finished name='Task-2' coro=<main() done, defined at example.py:13> result=None>
Task cancelled in main
Cela confirme que la tâche a été annulée par la coroutine main()
. Le bloc except asyncio.CancelledError
permet le nettoyage avant que la tâche ne soit complètement terminée, empêchant les fuites de ressources ou un état incohérent.
4. Gestion des exceptions dans les coroutines
Une gestion appropriée des exceptions est essentielle dans le code asynchrone. Considérez l’exemple suivant avec une exception non gérée :
import asyncio
async def divide(x, y):
return x / y
async def main():
result = await divide(10, 0)
print(f"Result: {result}")
if __name__ == "__main__":
asyncio.run(main(), debug=True)
Le mode de débogage signalera une exception non gérée :
asyncio | Task exception was never retrieved
future: <Task finished name='Task-1' coro=<main() done, defined at example.py:6> result=None, exception=ZeroDivisionError('division by zero')>
Pour gérer cette exception, vous pouvez utiliser un bloc try...except
 :
import asyncio
async def divide(x, y):
return x / y
async def main():
try:
result = await divide(10, 0)
print(f"Result: {result}")
except ZeroDivisionError as e:
print(f"Error: {e}")
if __name__ == "__main__":
asyncio.run(main(), debug=True)
Maintenant, l’exception sera capturée et gérée avec élégance.
Meilleures pratiques pour le débogage Asyncio
Voici quelques meilleures pratiques pour le débogage du code asyncio
 :
- Activer le mode de débogage : Activez toujours le mode de débogage pendant le développement et les tests.
- Utiliser la journalisation : Ajoutez une journalisation détaillée à vos coroutines pour suivre leur flux d’exécution. Utilisez
logging.getLogger('asyncio')
pour les événements spécifiques à asyncio et vos propres enregistreurs pour les données spécifiques à l’application. - Gérer les exceptions : Mettez en œuvre une gestion robuste des exceptions pour empêcher les exceptions non gérées de planter votre application.
- Utiliser les groupes de tâches (Python 3.11+) : Les groupes de tâches simplifient la gestion des exceptions et l’annulation dans les groupes de tâches connexes.
- Profiler votre code : Utilisez des outils de profilage pour identifier les goulots d’étranglement des performances.
- Écrire des tests unitaires : Écrivez des tests unitaires approfondis pour vérifier le comportement de vos coroutines.
- Utiliser des indications de type : Tirez parti des indications de type pour détecter les erreurs liées au type dès le début.
- Envisager d’utiliser un débogueur : Des outils tels que
pdb
ou les débogueurs IDE peuvent être utilisés pour parcourir le code asyncio. Cependant, ils sont souvent moins efficaces que le mode de débogage avec une journalisation minutieuse en raison de la nature de l’exécution asynchrone.
Techniques de débogage avancées
Au-delà du mode de débogage de base, considérez ces techniques avancées :
1. Politiques de boucle d’événements personnalisées
Vous pouvez créer des politiques de boucle d’événements personnalisées pour intercepter et enregistrer les événements. Cela vous permet d’obtenir un contrôle encore plus précis sur le processus de débogage.
2. Utilisation d’outils de débogage tiers
Plusieurs outils de débogage tiers peuvent vous aider à déboguer le code asyncio
, tels que :
- PySnooper : Un outil de débogage puissant qui enregistre automatiquement l’exécution de votre code.
- pdb++ : Une version améliorée du débogueur
pdb
standard avec des fonctionnalités améliorées. - asyncio_inspector : Une bibliothèque spécialement conçue pour inspecter les boucles d’événements asyncio.
3. Patchage Monkey (Ă utiliser avec prudence)
Dans les cas extrĂŞmes, vous pouvez utiliser le patchage monkey pour modifier le comportement des fonctions asyncio
à des fins de débogage. Cependant, cela doit être fait avec prudence, car cela peut introduire des bogues subtils et rendre votre code plus difficile à maintenir. Ceci est généralement déconseillé sauf si absolument nécessaire.
Conclusion
Le débogage du code asynchrone peut être difficile, mais le mode de débogage asyncio
fournit des outils et des informations précieuses pour simplifier le processus. En activant le mode de débogage, en interprétant la sortie et en suivant les meilleures pratiques, vous pouvez identifier et résoudre efficacement les problèmes courants dans vos applications asynchrones, ce qui conduit à un code plus robuste et plus performant. N’oubliez pas de combiner le mode de débogage avec la journalisation, le profilage et des tests approfondis pour obtenir les meilleurs résultats. Avec la pratique et les bons outils, vous pouvez maîtriser l’art du débogage des coroutines asyncio
et créer des applications asynchrones évolutives, efficaces et fiables.